package org.mp3weezel.strands;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;

import org.mp3weezel.strands.domain.*;

public class DefaultStrandsService implements StrandsService {

	private static final String BASE_PROTOCOL = "https";
	private static final String BASE_HOST = "www.mystrands.com";
	private static final String BASE_FILE = "/services";
	
	private StrandsAPI api;
	
	private static boolean initialized = false;
	
	private String subscriberId;
	
	public DefaultStrandsService(StrandsAPI api, String subscriberId) {
		this.api = api;
		this.subscriberId = subscriberId;
	}
	
	@Override
	public void prefetch() {
		try {
			getJAXBContext();
		} catch (JAXBException e) {
			// ignore all exceptions while prefetching
		}
		init();
	}
	
	@Override
	public InputStream requestFile(String uri, Map<String, String[]> parameters)
			throws StrandsServiceException {
		// currently not supported
		return null;
	}
	
	@Override
	public int requestHttp(String uri, Map<String, String[]> parameters)
			throws StrandsServiceException {
		return (Integer)handleRequest(uri, parameters, new RequestHandler() {
			@Override
			public Object handleRequest(HttpURLConnection conn) throws Exception {
				return conn.getResponseCode();
			}
		});
	}
	
	@Override
	public Object request(String uri, Map<String, String[]> parameters)
			throws StrandsServiceException {
		return handleRequest(uri, parameters, new RequestHandler() {
			@Override
			public Object handleRequest(HttpURLConnection conn) throws Exception {
				InputStream stream = conn.getInputStream();
				Object o = getJAXBContext().createUnmarshaller().unmarshal(stream);
				if (api.isDebug()) {
					System.out.println(printDomainObject("", null, o));
				}
				return o;
			}
		});
	}

	private Object handleRequest(String uri, Map<String, String[]> parameters, RequestHandler handler) throws StrandsServiceException {
		init();
		URL url = null;
		try {
			url = new URL(BASE_PROTOCOL, BASE_HOST, BASE_FILE
					+ buildRequestURL(uri, parameters));
		} catch (MalformedURLException e) {
		}
		if (api.isDebug()) {
			System.out.println("Requesting: " + url.toString());
		}
		
		HttpsURLConnection conn = null;
		InputStream stream = null;
		try {
			conn = (HttpsURLConnection) url.openConnection();
			return handler.handleRequest(conn);
		} catch (Exception e) {
			throw new StrandsServiceException(uri, parameters, e.getMessage(), e);
		} finally {
			// free all resources allocated by this request
			if (stream != null) {
				try {
					stream.close();
				} catch (IOException e) {
					// ignore if stream close failed
				}
			}
			if (conn != null) {
				conn.disconnect();
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private StringBuffer printDomainObject(String originalPrefix, StringBuffer b, Object o) {
		if (b == null) {
			b = new StringBuffer();
		}
		String className = o.getClass().getName();
		b.append(className.substring(className.lastIndexOf('.')+1,className.length())); 
		
		String prefix = originalPrefix+ "  ";
		
		Field[] fields = o.getClass().getDeclaredFields();
		b.append(" {");
		
		int maxNameLength = 0;
		for (Field field : fields) {
			if (field.getName().length() > maxNameLength) {
				maxNameLength = field.getName().length();
			}
		}
		
		for (Field field : fields) {
			field.setAccessible(true);
			b.append("\n");
			b.append(prefix);
			b.append(printName(maxNameLength, field.getName()));
			b.append(" = ");
			Object value;
			try {
				value = field.get(o);
				if (value instanceof String) {
					b.append(value.toString());
				} else if (value == null){
					b.append("null");
				} else if (value instanceof java.util.List<?>){
					b.append("List {");
					String internalOriginalPrefix = prefix;
					prefix = prefix+ "  ";
					java.util.List<Object> list = (java.util.List<Object>)value;
					for (Object object : list) {
						b.append("\n");
						b.append(prefix);
						printDomainObject(prefix, b, object);
					}
					b.append("\n").append(internalOriginalPrefix).append("}");
				} else {
					printDomainObject(prefix, b, value);
				}
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
			
		}
		b.append("\n").append(originalPrefix).append("}");
		return b;
		
	}
	
	private String printName(int maxLength, String name) {
		StringBuffer b = new StringBuffer();
		b.append(name);
		int spacesTofill = maxLength - name.length();
		for (int i = 0; i < spacesTofill; i++) {
			b.append(" ");
		}
		return b.toString();
		
	}
	
	protected String buildRequestURL(String uri, Map<String, String[]> parameters) {
		StringBuffer url = new StringBuffer();
		if (!uri.startsWith("/")) {
			url.append("/");
		}
		url.append(uri);
		url.append("?subscriberId=" + subscriberId);
	
		for (String key : parameters.keySet()) {
			String[] values = parameters.get(key);
			if (values != null) {
				for (String value : values) {
					if (value != null) {
						url.append("&");
						url.append(key);
						url.append("=");
						try {
							url.append(URLEncoder.encode(value, "utf-8"));
						} catch (UnsupportedEncodingException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	
		return url.toString();
	}

	private static void init() {
		if (initialized) {
			return;
		}
		TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
			public java.security.cert.X509Certificate[] getAcceptedIssuers() {
				return null;
			}
	
			public void checkClientTrusted(
					java.security.cert.X509Certificate[] certs, String authType) {
			}
	
			public void checkServerTrusted(
					java.security.cert.X509Certificate[] certs, String authType) {
			}
		} };
		try {
			SSLContext sc = SSLContext.getInstance("SSL");
			// Create empty HostnameVerifier
			HostnameVerifier hv = new HostnameVerifier() {
				public boolean verify(String urlHostName, SSLSession session) {
					return true;
				}
			};
	
			sc.init(null, trustAllCerts, new java.security.SecureRandom());
			HttpsURLConnection
					.setDefaultSSLSocketFactory(sc.getSocketFactory());
	
			SSLSocketFactory sslSocketFactory = sc.getSocketFactory();
			HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
			HttpsURLConnection.setDefaultHostnameVerifier(hv);
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} 
		initialized = true;
	}

	private static JAXBContext context;
	
	@SuppressWarnings("unchecked")
	private static JAXBContext getJAXBContext() throws JAXBException {
		if (context == null) {
			Class[] classes = new Class[] {
				Advertisement.class,
				AdvertisementList.class,
				Advertiser.class,
				AdvertiserList.class,
				AlbumTrack.class,
				AlbumTrackList.class,
				CategoryList.class,
				Command.class,
				CommunityTags.class,
				Contact.class,
				ContactList.class,
				CountedTrack.class,
				CountedTrackList.class,
				Current.class,
				DetailedAlbum.class,
				DetailedAlbumList.class,
				DetailedPlaylist.class,
				DetailedPlaylistList.class,
				Historical.class,
				Id.class,
				IdMappingList.class,
				Indie.class,
				Instruction.class,
				Listeners.class,
				ListeningHistory.class,
				ListeningHistoryList.class,
				MediaItem.class,
				MediaItemList.class,
				Message.class,
				MessageCount.class,
				MessageList.class,
				MobileOperator.class,
				MobileOperatorList.class,
				NewUser.class,
				NewUserArtist.class,
				NewUserContent.class,
				ObjectFactory.class,
				OnlineUser.class,
				OnlineUserList.class,
				Past.class,
				PersonalTags.class,
				PlayEvent.class,
				PlayEventList.class,
				Playlist.class,
				PlaylistList.class,
				Random.class,
				Recommendations.class,
				RecommendedTrackList.class,
				RecommendedTrackListList.class,
				RecommendedUser.class,
				RecommendedUserList.class,
				RecommendedVenue.class,
				RecommendedVenueList.class,
				Relation.class,
				RelationList.class,
				ServicePageInfo.class,
				Session.class,
				SessionAdHistory.class,
				SessionAdHistoryList.class,
				SessionAttribute.class,
				SessionAttributeList.class,
				SessionFilter.class,
				SessionFilterList.class,
				SessionList.class,
				SessionPlayHistory.class,
				SessionPlayHistoryList.class,
				SessionRated.class,
				SessionRatedList.class,
				SessionResume.class,
				SessionTopArtist.class,
				SessionTopArtistsList.class,
				SessionTrack.class,
				SessionTrackList.class,
				SessionUser.class,
				SessionUserArtist.class,
				SessionUserArtistList.class,
				SessionUserAttribute.class,
				SessionUserAttributeList.class,
				SessionUserContent.class,
				SessionUserContentList.class,
				SessionUserList.class,
				SessionUserMessagesCount.class,
				SessionUserMessagesCountList.class,
				SimpleAlbum.class,
				SimpleAlbumList.class,
				SimpleAlbumListList.class,
				SimpleArtist.class,
				SimpleArtistList.class,
				SimpleArtistListList.class,
				SimpleArtistWithAlbums.class,
				SimpleArtistWithAlbumsList.class,
				SimpleGenre.class,
				SimpleGenreList.class,
				SimpleTrack.class,
				SimpleTrackList.class,
				SimpleTrackListList.class,
				SmsInstructions.class,
				Source.class,
				SourceTrack.class,
				Sponsored.class,
				SummaryEvent.class,
				SummaryEventList.class,
				Tag.class,
				TagList.class,
				TagListList.class,
				Tags.class,
				Text.class,
				Texts.class,
				TrackSummaryDisplay.class,
				Urls.class,
				User.class,
				UserAdd.class,
				UserLibrary.class,
				UserLibraryList.class,
				UserList.class,
				UserSessionText.class,
			};
				context = JAXBContext.newInstance(classes);
		}
		return context;
	}
	
	private static interface RequestHandler {
		Object handleRequest(HttpURLConnection connection) throws Exception;
	}
}
